/**
 * Copyright 2018 orivil.com. All rights reserved.
 * Use of this source code is governed by a MIT-style
 * license that can be found at https://mit-license.org.
 */

package user_view

import (
	"sync"
	"time"
	"gitee.com/tugoer/orivil/mux"
	"github.com/jinzhu/gorm"
	"gitee.com/tugoer/orivil/log"
	"gitee.com/tugoer/orivil/bundles/db"
)

var Container = initContainer()

const (
	// 自动备份到数据库的等待时间
	SaveTickerSection = 10 * time.Minute

	// 每保存一个数据所等待的时间，用以减小数据库并发压力
	SaveWaitTime = 100 * time.Millisecond

	DateTimeLayout = "2006-01-02"
)

type targetID int

//type userIP string

type container struct {
	// 数据库中当天的数据
	exist map[targetID]*UserView

	// 当天的 ip 列表
	//uv map[targetID]map[userIP]struct{}

	// 当天的整点时间
	date *time.Time

	// 持久化数据到数据库的传送管道
	uvChan chan *UserView

	locker sync.RWMutex
}

// 发送到存储通道中
func (c *container) save() {
	c.locker.Lock()
	defer c.locker.Unlock()
	for _, userView := range c.exist {
		// 检测上一次保存时的数据，如果没有变化则跳过保存
		if userView.changed {
			// 发送到数据库存储通道
			c.uvChan <- userView
			userView.changed = false
		}
	}
}

// 数据清零
func (c *container) initData(now time.Time) {
	// 保存到数据库中
	c.save()

	c.locker.Lock()
	defer c.locker.Unlock()

	// 清零所有数据
	//c.uv = make(map[targetID]map[userIP]struct{}, 1000)
	c.exist = make(map[targetID]*UserView, 100)

	// 初始化时间为当天整点
	nowStr := now.Format(DateTimeLayout)
	date, err := time.Parse(DateTimeLayout, nowStr)
	if err != nil {
		panic(err)
	}
	c.date = &date
}

// 触发一次点击事件，事件根据 tID 及 当前 零点 时间作为 唯一判定标准
func (c *container) Click(label, tID int, gotCookie bool) (err error) {
	c.locker.Lock()
	defer c.locker.Unlock()
	tid := targetID(tID)
	//uip := userIP(uIP)
	var userView *UserView
	// 查询缓存
	if userView = c.exist[tid]; userView == nil {
		// 查询数据库
		userView, err = GetUserView(c.date, tID)
		if err != nil {
			// 创建新数据
			if err == gorm.ErrRecordNotFound {
				userView = &UserView {
					Label: label,
					TargetID: tID,
					Date: c.date,
				}
				err = userView.Create()
				if err != nil {
					return err
				}
			} else {
				return err
			}
		}
		// 保存数据库数据到缓存
		c.exist[tid] = userView
	}
	userView.selfAddPV()
	if !gotCookie {
		userView.selfAddUV()
	}
	return nil
}

func initContainer() *container {
	c := &container{}
	c.initData(time.Now())
	c.uvChan = make(chan *UserView, 3000)

	cancelChan := make(chan struct{})
	done := make(chan struct{})
	mux.BeforeClose(func() {
		log.Info.Println("关闭统计服务...")
		cancelChan <- struct{}{}
		<-done
		log.Info.Println("统计服务已关闭")
	})

	// 每隔一段时间将数据保存到数据库中
	go func() {
		ticker := time.NewTicker(SaveTickerSection)

		// 小时内分钟数，已确保 零点 以后只执行一次初始化任务
		minutSection := int((SaveTickerSection/time.Minute)%60)
		for {
			select {
			case <-cancelChan:
				// 保存已有数据
				c.save()
				// 发送关闭信号
				c.uvChan <- &UserView{ID: -1}
				return
			case t := <-ticker.C:
				log.Info.Println("开始备份统计到数据库...")
				hour, minute := t.Hour(), t.Minute()
				// 凌晨0点
				if hour == 0 && minutSection - minute >=0 {
					log.Info.Println("开始初始化数据...")
					c.initData(t)
				} else {
					c.save()
				}
			}
		}
	}()

	// 存储到数据库
	go func() {
		for uv := range c.uvChan {
			if uv.ID != -1 {
				time.Sleep(SaveWaitTime)
				err := uv.Save(c.date)
				if err != nil {
					log.Emergency.Printf("存储 User View 数据时出错: %v\n", err)
				}
			} else {
				done <- struct{}{}
				return
			}
		}
	}()
	return c
}

type UserView struct {
	ID int
	UV int `gorm:"index"`
	PV int `gorm:"index"`
	TargetID int
	Label int `gorm:"index"`
	Date *time.Time `gorm:"index"`
	// 缓存当前数据在上次之后是否变化，变化则保存到数据库中
	changed bool
}

func GetUserView(date *time.Time, targetID int) (uv *UserView, err error) {
	uv = &UserView{}
	err = db.Gorm.Where("target_id=? AND date=?", targetID, date).First(uv).Error
	return
}

func CountLabel(minDate, maxDate *time.Time, label int) (total int, err error) {
	model := where(minDate, maxDate, label)
	err = model.Count(&total).Error
	return
}

func where(minDate, maxDate *time.Time, label int) *gorm.DB {
	model := db.Gorm.Model(&UserView{})
	if minDate != nil && maxDate != nil {
		model = model.Where("date>=? AND date<=? AND label=?", minDate, maxDate, label)
	} else if minDate != nil {
		model = model.Where("date>=? AND label=?", minDate, label)
	} else if maxDate != nil {
		model = model.Where("date<=? AND label=?", maxDate, label)
	} else {
		model = model.Where("label=?", label)
	}
	return model
}

func QueryUserViews(minDate, maxDate *time.Time, label, limit, offset int, order string) (uvs []*UserView, err error) {
	model := where(minDate, maxDate, label)
	if order != "" {
		model = model.Order(order)
	}
	err = model.Limit(limit).Offset(offset).Find(&uvs).Error
	return
}

func (uv *UserView) selfAddPV() {
	uv.changed = true
	uv.PV++
}

func (uv *UserView) selfAddUV() {
	uv.changed = true
	uv.UV++
}

func (uv *UserView) Create() error {
	return db.Gorm.Create(uv).Error
}

func (uv *UserView) Save(today *time.Time) error {
	return db.Gorm.Model(UserView{}).Where("id=?", uv.ID).Updates(&UserView{PV: uv.PV, UV: uv.UV}).Error
}

func (uv *UserView) Delete() error {
	return db.Gorm.Model(UserView{}).Where("id=?", uv.ID).Delete(uv).Error
}